On this page
- Drush and the Drupal core CLI script have a modern successor!
- Key differences between Drupal CLI and Drush
- What Drush Command Writers Should Know
- Use the Service Wrapper Pattern to Make Commands Compatible with Both
- Step 1: Create the Business Logic Service
- Step 2: Create the Drupal Core CLI Command (vendor/bin/dr)
- Step 3: Create the Drush Command (vendor/bin/drush)
- Why this is the best approach
- Naming Conventions and Best Practices
Drush command porting guide to the new dr Drupal core CLI
This documentation needs review. See "Help improve this page" in the sidebar.
Drush and the Drupal core CLI script have a modern successor!
Drupal core's command-line capabilities are expanding with the introduction of a native extensible Drupal Core CLI. Historically, core/scripts/drupal provided a limited command-line interface for core-specific tasks, while the community-maintained Drush project became the de facto standard for Drupal site management. To standardize and improve developer experience, Drupal has introduced a new, built-in CLI accessible via vendor/bin/dr. This new implementation refactors existing core commands and provides a standardized way for module developers to create discoverable commands using modern Symfony attributes, effectively becoming a replacement for both the previously core-provided script and the robust functionality previously exclusive to Drush.
There were only a few Drupal core commands before and most developers would be familiar with Drush commands. Many Drupal extensions provide them. This guide aims to provide information on how to port Drush commands to the new core solution and how to keep commands compatible with both the core solution and Drush in the transition period.
Both Drupal Core CLI (vendor/bin/dr) and Drush are built on top of the Symfony Console component, but they handle command registration, annotations/attributes, and boilerplate slightly differently.
Here is a comparison, what you need to know as a Drush command developer, and how to maintain compatibility between both ecosystems.
Key differences between Drupal CLI and Drush
|
Feature |
Drupal Core CLI ( |
Drush ( |
|
Directory |
|
|
|
Base Class |
Extends the Symfony Command component class OR is a plain class with |
Typically extends |
|
Registration |
Auto-discovered by Drupal's Compiler Pass. No |
Registered explicitly in |
|
Attributes |
Uses Symfony attributes ( |
Uses Drush/Consolidation attributes ( |
|
Execution Method |
|
Any custom method name mapped to the command via the attribute. |
What Drush Command Writers Should Know
If you are used to writing Drush commands, moving to the new Drupal Core CLI introduces a few changes:
-
No
drush.services.ymlrequired: In Drush, you usually have to wire up your command class in a specificdrush.services.ymlfile. In the new Drupal Core CLI, any class inside your module'ssrc/Command/directory that uses the#[AsCommand]attribute is automatically scanned, autowired, and registered viaConsoleCompilerPass. -
Invokable Classes: Drush command files often contain multiple commands (multiple methods inside one
DrushCommandsclass). Drupal's new standard highly encourages Single Responsibility using the__invoke()method on a plain class, creating one class per command. -
Arguments and Options Injection: In Drush, arguments and options are passed directly as parameters to your method (e.g., public function
myCommand($name, $options = ['shout' => false])). In Drupal Core CLI's__invokepattern, you can use PHP attributes directly on the parameters (e.g.,#[Argument('The name')] string $name). -
Interactive Prompts Built-in: Core CLI utilizes the new Symfony
#[Ask]attribute for interactive prompts natively on the parameter definition. In Drush, you typically have to drop down into$this->io()->ask()inside the method body. -
Return Types: Standard Symfony commands must return an integer (
Command::SUCCESSorCommand::FAILURE). Drush commands are heavily integrated withConsolidation/OutputFormattersand frequently return arrays, objects, or strings that Drush automatically formats into tables or JSON based on the user's--formatflag.
Use the Service Wrapper Pattern to Make Commands Compatible with Both
Because Drush and Drupal Core CLI expect different metadata (Symfony attributes vs. Consolidation attributes) and slightly different execution flows (returning an int vs returning formatted data), a single class acting as both a Drush command and a Core CLI command is generally discouraged and very messy to maintain.
If you want to provide your command to both ecosystems, the best practice is the Service Wrapper Pattern. Extract the actual business logic of your command into a standard Drupal Service. Then, write two very thin "wrapper" command classes—one for Drush and one for Core CLI.
Step 1: Create the Business Logic Service
// src/MyModuleLogic.php
namespace Drupal\my_module;
class MyModuleLogic {
public function doSomething(string $name): string {
return "Hello, $name!";
}
}
(Register this normally in my_module.services.yml)
Step 2: Create the Drupal Core CLI Command (vendor/bin/dr)
// src/Command/HelloCommand.php
namespace Drupal\my_module\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;
use Drupal\my_module\MyModuleLogic;
#[AsCommand(name: 'my_module:hello', description: 'Says hello.')]
class HelloCommand {
public function __construct(private readonly MyModuleLogic $logic) {}
public function __invoke(OutputInterface $output, #[Argument] string $name): int {
$result = $this->logic->doSomething($name);
$output->writeln($result);
return Command::SUCCESS;
}
}
Step 3: Create the Drush Command (vendor/bin/drush)
// src/Drush/Commands/HelloDrushCommands.php<
namespace Drupal\my_module\Drush\Commands;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;
use Drupal\my_module\MyModuleLogic;
class HelloDrushCommands extends DrushCommands {
public function __construct(private readonly MyModuleLogic $logic) {
parent::__construct();
}
#[CLI\Command(name: 'my_module:hello', aliases: ['mm-hello'])]
#[CLI\Argument(name: 'name', description: 'The name to greet')]
public function sayHello(string $name) {
$result = $this->logic->doSomething($name);
$this->logger()->success($result);
}
}
(Register this in drush.services.yml)
Why this is the best approach
-
Separation of Concerns: Your business logic remains independent of the CLI framework calling it.
-
Formatting features: Drush has advanced table formatting and pipeline outputs that Symfony standard CLI doesn't natively share out-of-the-box in the same way. By splitting them, you can utilize Drush's rich formatting in the Drush wrapper, and standard Symfony formatting in the Core wrapper.
-
Future Proof: If either Drush or Drupal Core updates their command implementation standards, you only have to update the thin wrapper files, not your core logic.
Naming Conventions and Best Practices
-
Naming Conventions: Modules must use their module name as the namespace, substituting underscores with hyphens (e.g.,
my_modulebecomesmy-module:my-command). Thecoreand core module namespaces are reserved for Drupal core to prevent future conflicts. -
Logging: The dr CLI logs output to the logger.console (specifically
Drupal\Core\Command\DrupalConsoleLogger). -
Status and Support: The new CLI is currently considered
@internaland experimental. For support or to track progress, join the#cli-in-corechannel on Drupal Slack or follow discussions in the [meta] CLI in Core community initiative issue.
Help improve this page
You can:
- Log in, click Edit, and edit this page
- Log in, click Discuss, update the Page status value, and suggest an improvement
- Log in and create a Documentation issue with your suggestion